package org.example.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.annotation.SlideWindowLimit;
import org.example.exception.RateLimitException;
import org.example.properties.RateLimitProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author liyishan
 * @date 2024/1/10 16:17
 * @apiNote
 */
@Aspect
@Component
@Slf4j
public class SlideWindowLimitAspect {

    @Autowired
    private RateLimitProperties limitProperties;

    @Autowired
    private  RedisTemplate<String, Long> redisTemplate;

    @Pointcut("@annotation(org.example.annotation.SlideWindowLimit)")
    protected void lockPoint(){
    }

    @Around("lockPoint()")
    public Object round(ProceedingJoinPoint point)throws Throwable{
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        // 获取LeakyBucketLimit注解，如果方法和类上同时出现，以方法上的为准
        Class<?> clazz = method.getDeclaringClass();
        SlideWindowLimit slideWindowLimit;
        slideWindowLimit = method.getAnnotation(SlideWindowLimit.class);
        if (slideWindowLimit == null) {
            slideWindowLimit = clazz.getAnnotation(SlideWindowLimit.class);
            if (slideWindowLimit == null) {
                return point.proceed();
            }
        }

        // 根据用户IP（可选）和接口方法，构造key
        String key = limitProperties.getKey(slideWindowLimit.value(), slideWindowLimit.key(), clazz.getSimpleName(), method.getName(), limitProperties.getStrategyConfig().getSlideWindowLimitStrategy());

        // 限流逻辑实现
        ZSetOperations<String, Long> zSetOperations = redisTemplate.opsForZSet();
        // 记录本次访问的时间结点
        long currentTimeMillis = System.currentTimeMillis();
        zSetOperations.add(key, currentTimeMillis, currentTimeMillis);
        // 这一步是为了防止一直存在于内存中
        int time = slideWindowLimit.time();
        redisTemplate.expire(key, slideWindowLimit.time(), TimeUnit.SECONDS);
        // 移除{time}秒之前的访问记录（滑动窗口思想）
        zSetOperations.removeRangeByScore(key, 0, currentTimeMillis - time * 1000L);

        // 获得当前窗口内的访问记录数
        Long curCount = zSetOperations.zCard(key);
        // 限流判断
        if (curCount != null && curCount > slideWindowLimit.count()) {
            throw new RateLimitException("请求过于频繁，请稍后重试！");
        }
        Object object = point.proceed();
        return object;
    }

}
